El electroecenfalograma (EEG) es un análisis que se utiliza para detectar aomalías relacionadas con la actividad eléctrica del cerebro. Este procedimiento realiza un seguimiento de las ondas cerebrales y las registra, mediante la colocación de discos metálicos con cables delgados (electrodos) sobre el cuero cabelludo y se envían señales a una computadora para registrar los resultados. La actividad eléctrica normal del cerebro forma un patrón reconocible. Por medio de un EEG es posible buscar patrones anormales que indiquen convulsiones u otros problemas, permitiendo así el diagnóstico y el control de los trastornos convulsivos.
En la actualidad, un neurólogo es el encargado de leer e interpretar los resultados obtenidos por el electroecenfalograma, los cuales varían en complejidad y duración; ya que puede verse alterado por el comportamiento que tenga el paciente durante el examen. Debido a esto, no cabe duda que el diagnóstico esté altamente ligado al criterio profesional del médico. Con el fin de reducir el error o la influencia de la interpretación humana en el diagnóstico del paciente, se propone realizar un modelo de clasificación que permita determinar si el paciente presenta durante la realización del examen, algún indicio de ataque convulsivo. De este modo, es posible desarrollar un "estándar", con una alta confiabilidad (superior al 85%), en el diagnóstico del paciente.
Este proyecto parte de la hipótesis de que es posible, mediante un modelo de clasificación, determinar si el sujeto se encuentra bajo una actividad convulsiva, esto basado en las lecturas registradas por el EEG; por lo que se tiene como objetivo el desarrollo de un modelo de clasificación que permita obtener un diagnóstico, superior al 85% en confiabilidad, para el paciente, partiendo de los resultados obtenidos en el EEG realizado.
El conjunto de datos con el que se desarrolla el proyecto es una serie temporal que muestrea en 4097 puntos de datos un conjunto de referencia que consta de 5 carpetas diferentes, cada una con 100 archivos, donde cada archivo representa un solo sujeto o paciente. Cada uno de estos archivos del conjunto de datos de referencia es una grabación cerebral durante 23.6 segundos. Cada punto de datos es el valor de la grabación del electroecenfalograma (EEG) en un punto diferente en el tiempo; por lo que el conjunto de datos con el que se trabaja corresponde a 4097 puntos de datos durante 23.5 segundos para cada uno de los 500 individuos en análisis.
Estos 4097 puntos de datos se dividen en 23 fragmentos que contienen 178 puntos de datos durante 1 segundo, donde cada uno estos puntos de datos es el valor de la grabación del EEG en un momento diferente. Con esto, finalmente se tiene un conjunto de datos conformado por 11500 piezas de información (filas) y, cada una de estas contiene un total de 178 puntos de datos por 1 segundo conformando las columnas. Además, se cuenta con una columna adicional representada con la etiqueta "y" correspondiente a la variable de respuesta, conformada por valores numéricos enteros que indican lo siguiente:
Para el caso en el que los pacientes se ubiquen en las clases 2, 3, 4 y 5; indica que son sujetos que no tuvieron crosos epilépticas. Úncamente los sujetos que se ubican en la categoría 1 tuvieron crisis epilépticas.
A pesar de que se cuentan con 5 clases distintas en la variable de respuesta, se va a realizar el análisis con una clasificación binaria que determine si el sujeto registró o no alguna actividad epiléptica.
Inicialmente, se procede a llamar todos los módulos que se necesitan para la realización de la tarea:
# imports
# numpy, pandas, scipy, math, matplotlib
import numpy as np
import pandas as pd
import scipy
import seaborn as sns
from math import sqrt
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
# estimators
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
# model metrics
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, cohen_kappa_score
from sklearn.model_selection import cross_val_score
# cross validation
#from sklearn.cross_validation import train_test_split
from sklearn.model_selection import train_test_split
# PCA
from sklearn.decomposition import PCA
Se importa el conjuto de datos con el que se lleva a cabo el análisis:
# data
EEG_DF = pd.read_csv('Reconocimiento_de_Ataques_Epilepticos.csv')
EEG_DF.head()
Se verifica si existe algún valor faltante dentro del conjunto de datos:
EEG_DF.isnull().sum().sort_values()
De lo anterior, se puede observar que no existen valores "nulos" dentro del conjunto de datos, por lo que se puede seguir con el análisis del mismo.
La primer columna no aporta ningún tipo de información, por lo que se procede a la eliminación de esta:
EEG_DF = EEG_DF.drop(columns=['Unnamed: 0'])
EEG_DF.head()
Se examina la estructura de los datos y el tipo de variables de los mismos:
EEG_DF.info()
De lo anterior, se observa que todas las variables corresponden a datos de tipo numérico. Sin embargo, hay que tener en cuenta que a pesar de que la variable de respuesta 'y' está representada por valores numéricos, estos representan categorías. Como se menciona anteriormente, el enfoque de este análisis es únicamente determinar si el sujeto registró o no alguna actividad epiléptica, por lo que, además de cambiar la variable 'y' como categórica, se sustituyen los valores 2, 3, 4 y 5, como categoría 0, la cual estaría indicando que el paciente no registró actividad convulsiva.
EEG_DF['y'] = EEG_DF.loc[:, 'y'].replace([2, 3, 4, 5], 0)
EEG_DF.head()
Se convierte la variable de respuesta 'y' a variable de tipo nominal o categórica:
EEG_DF = EEG_DF.astype({'y': 'category'})
EEG_DF.info()
Definidas las categorías de la variable de respuesta, se analiza estadísticamente, mediante la función describe(), el conjunto de datos separando los casos en los que se registran estados epilépticos y los que no.
# Description of Non Epileptic Registers
EEG_DF[EEG_DF['y'] == 0].describe()
# Description of Epileptic Registers
EEG_DF[EEG_DF['y'] == 1].describe()
De lo anterior, se pueden observar cambios significativos para los casos en el que se registra epilepsia con respecto a los que no. Esto se analizará a fondo más adelante.
A continuación, se generan diferentes visualizaciones con el fin de comprender mejor el conjunto de datos.
Inicialmente, se visualiza mediante un gráfico de pie la cantidad de registros que se tienen para casos no epilépticos y epilépticos.
countEpilepticReg = pd.Series(EEG_DF['y'].value_counts(dropna=False))
labels = "Non Epileptic", "Epileptic"
sizes = [countEpilepticReg.iloc[0], countEpilepticReg.iloc[1]]
explode = (0, 0.1) # only 'explode' the 2nd slice
plt.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=60)
plt.show()
Se puede observar que únicamente el 20% del conjunto de datos analizado registran casos epilépticos.
Ahora bien, con el fin de comprender mejor estadísticamente el conjunto de datos, se realiza un 'box plot' del promedio de los valores de los registros por el EEG para cada uno de los casos analizados: epilépticos y no epilépticos.
B = [EEG_DF[EEG_DF['y'] == 0].describe().T['mean'], EEG_DF[EEG_DF['y'] == 1].describe().T['mean']]
plt.boxplot(B,0,'gD', labels = labels)
plt.show()
Como se puede observar en el gráfico anterior, el promedio de las mediciones de los casos que no presentan ataques convulsivos se concentran en un rango muy pequeño entre -10 y -5; mientras que para los casos que registran epilepsia, el promedio varia considerablemente, donde la mayor concentración de datos se encuentran entre -12 y 2 aproximadamente, llegando a alcanzar las mediciones inferiores a -25 o superiores a 10 en algunos casos.
Tomando 2 ejemplos de fragmentos de cada uno de los casos en estudio, se procede a graficar un caso en el que el paciente no presente señales de ataque convulsivo contra otro que si con el fin de visualizar las diferencias en ambos casos.
non_epileptic_patch = mpatches.Patch(color='blue', label='Non Epileptic')
epileptic_patch = mpatches.Patch(color='orange', label='Epileptic')
plt.legend(handles=[non_epileptic_patch,epileptic_patch])
[plt.plot(EEG_DF.values[i][1:-1]) for i in range(2)];
plt.legend(handles=[non_epileptic_patch,epileptic_patch])
[plt.plot(EEG_DF.values[i+7][1:-1]) for i in range(2)];
Ambas visualizaciones confirman lo antes analizado en el 'box plot', en donde se puede observar claramente que para los casos en el que se registra algún ataque convulsivo los niveles de tensión varían entre rangos mucho mayores en comparasión con aquellos que no lo presentan.
Además, se observa una variación aleatoria y más pronunciada de las señales de medición a través del tiempo para los casos en el que se etiqueta como epiléptico. Partiendo de esto, se procede a analizar gráficamente la desviación estándar obtenida para cada uno de los puntos de datos los casos analizados.
B = [EEG_DF[EEG_DF['y'] == 0].describe().T['std'], EEG_DF[EEG_DF['y'] == 1].describe().T['std']]
plt.boxplot(B,0,'gD', labels = labels)
plt.show()
Se puede observar una clara diferencia entre ambos casos en análisis. Para el caso de en el que el paciente experimenta un ataque convulsivo, la desviación estándar de los datos se ubica en valores cercanos a 340; mientras que para aquellos en el que el paciente no registra el padecimiento, este valor se encuentra alrededor de 70.
Finalmente, se analiza el valor registrado por el EEG para ambas categorías en estudio mediante un gráfico de dispersión, analizando los valores máximos y mínimos alcanzados en cada uno de los fragmentos en los que se divide el conjunto de datos.
Analizando los valores mínimos:
sns.set(style="whitegrid")
sns.stripplot(x=EEG_DF['y'], y=np.min(EEG_DF, axis=1), jitter=0.4)
Analizando los valores máximos:
sns.stripplot(x=EEG_DF['y'], y=np.max(EEG_DF, axis=1), jitter=0.4)
Se puede observar una diferencia entre los valores máximos y mínimos alcanzados en las mediciones del EEG para las dos categorías en estudio; siendo la categoría que registra epilepsia aquella que alcanza los valores más altos y bajos en las mediciones.
Analizando la relación entre los valores máximos y mínimos para cada fragmento de información:
#lists of arrays containing all the EEG measures
not_epileptic = [EEG_DF[EEG_DF['y']==0].iloc[:, range(0, len(EEG_DF.columns)-1)].values]
epileptic = [EEG_DF[EEG_DF['y']==1].iloc[:, range(0, len(EEG_DF.columns)-1)].values]
#defining a function to calculate indicators
def indic(EEG_DF):
min = np.min(EEG_DF, axis=1)
max = np.max(EEG_DF, axis=1)
return max, min
x1,y1 = indic(not_epileptic)
x2,y2 = indic(epileptic)
fig = plt.figure(figsize=(10,4))
ax1 = fig.add_subplot(111)
ax1.scatter(x1, y1, s=15, c='b', label='Not Epiliptic')
ax1.scatter(x2, y2, s=15, c='r', label='Epileptic')
plt.legend(loc='lower left');
plt.show()
Se puede observar una clara separación en la relación entre ambas variables, lo cual puede ser de utilidad al momento de entrenar el modelo de clasificación. Debido a esto, se decide crear un nuevo atributo al conjunto de datos denominada 'Delta max-min' la cual corresponde al valor máximo registrado en ese fragmento menos el valor mínimo alcanzado.
max = EEG_DF.max(axis=1)
min = EEG_DF.min(axis=1)
delta = max-min
EEG_DF.insert(178, column='Delta max-min',value=delta)
B = [EEG_DF[EEG_DF['y'] == 0]['Delta max-min'], EEG_DF[EEG_DF['y'] == 1]['Delta max-min']]
plt.boxplot(B,0,'gD', labels = labels)
plt.show()
Se procede a analizar la correlación entre los datos, para ello, se cambia temporalmente la variable 'y' como numérico:
EEG_DF = EEG_DF.astype({'y': 'int64'})
corrMat = EEG_DF.corr()
corrMat
Obteniendo el mapa de calor de la respectiva matríz de correlación:
plt.figure(figsize=(25,25))
sns.heatmap(corrMat, annot=True, cmap=plt.cm.Reds)
plt.show()
El resultado del mapa de calor anterior es complejo de leer, por lo que no puede ser relevante en nuestro análisis. Por esto, se procede a resaltar aquellas variables dependientes que tienen una mayor correlación con la variable 'y':
cor_target = abs(corrMat['y'])
#Selecting highly correlated features
relevant_features = cor_target[cor_target>0.1]
relevant_features
Se observa que ninguna de las variables en el conjunto de datos en estudio tienen una alta correlación con la variable dependiente, por lo que se decide no extraer ninguno de los atributos del conjunto de datos. Cabe resaltar que aquella variable que tiene una mayor correlación con la variable 'y' corresponde a la variable creada anteriormente, lo cual puede indicar que su incorporación al conjunto de datos fue conveniente.
Se devuelve la variable dependiente como categórica para continuar con el análisis:
EEG_DF = EEG_DF.astype({'y': 'category'})
Una vez cargado el conjunto de datos, se procede a seleccionar las características con las que se desarrollará el modelo y la variable dependiente:
#features
features = EEG_DF.iloc[:,0:179]
print('Summary of feature sample')
features.head()
#dependent variable
depVar = EEG_DF['y']
Se procede ahora a utilizar PCA con el objetivo de reducir la dimensionalidad del conjunto de datos. Se decide utilizar un mínimo de número de componentes principales que contengan al menos un 95% de la varianza de la siguiente manera:
# Make an instance of the Model
pca = PCA(.95)
#Fit
pca.fit(features)
#Transform
features = pca.transform(features)
features
features_df = pd.DataFrame(features, columns=['PC0', 'PC1', 'PC2', 'PC3', 'PC4', 'PC5', 'PC6', 'PC7', 'PC8', 'PC9', 'PC10',
'PC11', 'PC12', 'PC13', 'PC14', 'PC15', 'PC16', 'PC17', 'PC18', 'PC19', 'PC20',
'PC21', 'PC22', 'PC23', 'PC24', 'PC25', 'PC26', 'PC27', 'PC28', 'PC29', 'PC30',
'PC31', 'PC32', 'PC33', 'PC34', 'PC35', 'PC36', 'PC37', 'PC38'])
features_df.head()
Como resultado, se reduce el conjunto de datos de 179 atributos a 39, los cuales representan un 95% de la varianza. Con este conjunto de datos se entrena el modelo. El conjunto de datos se divide en dos partes, un conjunto que contiene los diferentes atributos o variables independientes y otro conjunto que contiene la variable dependiente la cual se quiere predecir. Estos a su vez se dividen en conjuntos de entrenamiento (representando el 70% de los datos) y en conjuntos de prueba (los cuales contienen el 30% restante de los datos):
X_train, X_test, y_train, y_test = train_test_split(features_df, depVar, test_size=0.30, random_state=0)
Se verifica la división de los datos que se realizó:
X_train.shape, X_test.shape
En esta sección se crean los modelos de clasificación para seleccionar el adecuado de acuerdo a las necesidades.
Inicialmente, se establecen variables que se utilizarán para definir cada modelo con algunos parametros definidos:
#Models
modelDTC = DecisionTreeClassifier(criterion='gini', splitter='best', min_samples_split=2, max_depth=None)
modelKNN = KNeighborsClassifier(n_neighbors=15, weights='uniform')
modelSVC = SVC(kernel='rbf', gamma='scale')
modelRF = RandomForestClassifier(n_jobs= 10, n_estimators=120, criterion='gini', random_state=0)
Se verifica que la columna correcta se esté utilizando como variable dependiente:
#dependent variable
print(depVar)
Se utilizan las variables de modelo que se estableció anteriormente y se transfiere los datos de entrenamiento a todos los modelos de la siguiente manera:
#Decision Tree Classifier
modelDTC.fit(X_train,y_train)
#Random Forest Classifier
modelRF.fit(X_train,y_train)
#K-Nearest Neighbors
modelKNN.fit(X_train,y_train)
#Support Vector Classifier
modelSVC.fit(X_train,y_train)
En la mayoría de los casos es necesario la construcción de más de un modelo para encontrar el más apropiado para el trabajo. Sci-Kit Learn tiene una función que 'puntuará' la adecuación de cada modelo según el algoritmo utilizado para construirlo; los modelos con los puntajes más altos deben usarse para hacer predicciones. Como primer parámetro de medición del modelo se procede a obtener el valor del 'cross_val_score' de la siguiente manera:
#Decision Tree Classifier model
print(cross_val_score(modelDTC, X_train, y_train))
#Random Forest model
print(cross_val_score(modelRF, X_train, y_train))
#K-Nearest Neighbors model
print(cross_val_score(modelKNN, X_train, y_train))
#Support Vector Classifier model
print(cross_val_score(modelSVC, X_train, y_train))
A continuación, se procede a puntuar cada uno de los modelos creados:
#Decision Tree Classifier Model Scoring
modelDTC.score(X_train,y_train)
#Random Forest Model Scoring
modelRF.score(X_train,y_train)
#K-Nearest Neighbors Model Scoring
modelKNN.score(X_train,y_train)
#Support Vector Classifier Model Scoring
modelSVC.score(X_train,y_train)
Se puede observar que para los modelos de 'Decision Tree Classifier' y 'Random Forest' se tiene una puntuación de 1; lo cual podría estar indicando que este modelo esté inclinando las predicciones a una de las categorías, la cual probablemente corresponda a la categoría de casos en el que no se presenta una actividad convulsiva en el EEG, esto dado a que corresponde al 80% de los casos presentes en el conjunto de datos analizados. Por ello, se debe de prestar atención a estos modelos al momento de hacer las predicciones.
predictionsDTC = modelDTC.predict(X_test)
predictionsRF = modelRF.predict(X_test)
predictionsKNN = modelKNN.predict(X_test)
predictionsSVC = modelSVC.predict(X_test)
En este caso, dado que los modelos realizados corresponden a modelos de clasificación, se tomarán cómo parámetros de medición para elegir al modelo con mejor rendimiento la matriz de confusión, el reporte de clasificación de los resultados y el puntaje de precisión en las predicciones. Estos resultados se muestran a continuación:
#Evaluating Decision Tree Classifier Model
print("Confusion Matrix")
print(confusion_matrix(y_test, predictionsDTC))
print("Classification Report")
print(classification_report(y_test, predictionsDTC))
print("Accuracy Score")
print(accuracy_score(y_test, predictionsDTC))
print("Kappa")
print(cohen_kappa_score(y_test, predictionsDTC))
#Evaluating Random Forest Model
print("Confusion Matrix")
print(confusion_matrix(y_test, predictionsRF))
print("Classification Report")
print(classification_report(y_test, predictionsRF))
print("Accuracy Score")
print(accuracy_score(y_test, predictionsRF))
print("Kappa")
print(cohen_kappa_score(y_test, predictionsRF))
#Evaluating K-Nearest Neighbors Model
print("Confusion Matrix")
print(confusion_matrix(y_test, predictionsKNN))
print("Classification Report")
print(classification_report(y_test, predictionsKNN))
print("Accuracy Score")
print(accuracy_score(y_test, predictionsKNN))
print("Kappa")
print(cohen_kappa_score(y_test, predictionsKNN))
#Evaluating Support Vector Classifier Model
print("Confusion Matrix")
print(confusion_matrix(y_test, predictionsSVC))
print("Classification Report")
print(classification_report(y_test, predictionsSVC))
print("Accuracy Score")
print(accuracy_score(y_test, predictionsSVC))
print("Kappa")
print(cohen_kappa_score(y_test, predictionsSVC))
De los resultados anteriores, se puede observar que aquel modelo que presenta un mejor rendimiento corresponde al 'Support Vector Classifier' con un 97.5% de precisión en las predicciones y el valor de kappa más alto.
Cabe destacar que se supera considerablemente la confiabilidad planteada en el problema por lo que este modelo evidencia que es posible llevar a cabo el diagnóstico de un EEG de manera precisa.